<?php


namespace common\components\form;

use backend\components\Icon;
use yii\base\Component;
use yii\base\ErrorHandler;
use yii\validators\Validator;
use yii\web\JsExpression;
use yii\helpers\Html;
use yii\helpers\ArrayHelper;

/**
 * description of Field
 *
 * @author FireLoong
 */
class Field extends Component
{
    /**addAriaAttributes
     * @var Form 与此字段关联的表单。
     */
    public $form;
    /**
     * @var Model 与此字段关联的数据模型。
     */
    public $model;
    /**
     * @var string 与此字段关联的模型属性。
     */
    public $attribute;
    /**
     * @var string 字段类型
     */
    public $type;
    public $layout;
    /**
     * @var array 字段容器标记的HTML属性（名称-值对）。
     */
    public $options = ['class' => 'form-group'];
    /**
     * @var string 用于排列标签、输入字段、错误消息和提示文本的模板。
     */
    public $template = "{label}\n{input}\n{hint}\n{error}";
    /**
     * @var string|null 呈现{input}占位符内容的可选模板
     */
    public $inputTemplate;
    /**
     * @var array 输入标记的默认选项。传递给各个输入方法的参数
     */
    public $inputOptions = ['class' => 'form-control'];
    /**
     * @var array 包装标记的选项，用于`{beginWrapper}'占位符
     */
    public $wrapperOptions = [];
    /**
     * @var array CSS grid classes for horizontal layout. This must be an array with these keys:
     * - 'offset' the offset grid class to append to the wrapper if no label is rendered
     * - 'label' the label grid class
     * - 'wrapper' the wrapper grid class
     * - 'error' the error grid class
     * - 'hint' the hint grid class
     */
    public $horizontalCssClasses = [];
    /**
     * @var array `setup()` 函数的参数
     */
    public $setupParams = [];
    /**
     * @var array 字段数据
     */
    public $data = [];
    /**
     * @var array 错误标记的默认选项在呈现错误标记时，传递给[[error()]]的参数将与此属性合并。
     */
    public $errorOptions = ['class' => 'help-block'];
    public $labelOptions = ['class' => 'control-label'];
    public $hintOptions = ['class' => 'hint-block'];
    /**
     * @var bool 是否启用客户端数据验证。
     */
    public $enableClientValidation;
    /**
     * @var bool 是否启用基于AJAX的数据验证。
     */
    public $enableAjaxValidation;
    /**
     * @var bool 当输入字段的值更改时是否执行验证。
     */
    public $validateOnChange;
    /**
     * @var bool 当输入字段失去焦点时是否执行验证。
     */
    public $validateOnBlur;
    /**
     * @var bool 用户在输入字段中键入时是否执行验证。
     */
    public $validateOnType;
    /**
     * @var string 当用户在字段中键入且[[validateOnType]]设置为“true”时，验证应延迟的毫秒数。
     */
    public $validationDelay;
    /**
     * @var array 用于选择容器、输入和错误标记的jquery选择器。
     */
    public $selectors = [];
    /**
     * @var array 字段的不同部分（如input,label）。
     */
    public $parts = [];
    /**
     * @var bool 为 input 添加 aria html 属性“aria required”和“aria invalid”
     */
    public $addAriaAttributes = true;
    /**
     * @var bool 是否呈现错误。默认值为“true”，布局“inline”除外。
     */
    public $enableError = true;
    /**
     * @var bool 是否呈现 label。默认值为“true”。
     */
    public $enableLabel = true;
    /**
     * @var string 如果是使用[[inputOptions]]或在“input*”方法的“$options”参数中设置的，则此属性保留自定义输入ID。
     */
    protected $_inputId;
    /**
     * @var bool 是否应跳过“for”字段标签属性。
     */
    protected $_skipLabelFor = false;

    /**
     * {@inheritDoc}
     */
    public function __construct($config = [])
    {
        $layoutConfig = $this->createLayoutConfig($config);
        $config = ArrayHelper::merge($layoutConfig, $config);
        parent::__construct($config);
    }

    /**
     * 返回此对象的字符串表示形式的php magic方法。
     * @return string
     */
    public function __toString()
    {
        try {
            return $this->render();
        } catch (\Exception $e) {
            ErrorHandler::convertExceptionToError($e);
            return '';
        }
    }

    public function init()
    {
        if ($this->layout === 'horizontal') {
            $this->template = "{label}\n{beginWrapper}\n{input}\n{error}\n{endWrapper}\n{hint}";
            $this->horizontalCssClasses = array_merge([
                'offset' => 'col-sm-offset-3',
                'label' => 'col-sm-3',
                'wrapper' => 'col-sm-6',
                'error' => '',
                'hint' => 'col-sm-3',
            ], $this->horizontalCssClasses);
            $this->wrapperOptions['class'] = $this->horizontalCssClasses['wrapper'];
            $this->labelOptions['class'] = 'control-label ' . $this->horizontalCssClasses['label'];
            $this->errorOptions['class'] = trim('help-block help-block-error ' . $this->horizontalCssClasses['error']);
            $this->hintOptions['class'] = 'help-block ' . $this->horizontalCssClasses['hint'];
        } elseif ($this->layout === 'inline') {
            //$this->labelOptions['class'] = 'sr-only';
            $this->enableError = false;
        } elseif ($this->layout === 'default') {
            $this->options['class'] = 'form-group-default';
        }
    }

    /**
     * 渲染整个字段。
     * @param string|callable $content 字段容器中的内容。
     * @return string
     */
    public function render($content = null)
    {
        if ($content === null) {
            if (!isset($this->parts['{beginWrapper}'])) {
                $options = $this->wrapperOptions;
                $tag = ArrayHelper::remove($options, 'tag', 'div');
                $this->parts['{beginWrapper}'] = Html::beginTag($tag, $options);
                $this->parts['{endWrapper}'] = Html::endTag($tag);
            }
            if ($this->enableLabel === false) {
                $this->parts['{label}'] = '';
                $this->parts['{beginLabel}'] = '';
                $this->parts['{labelTitle}'] = '';
                $this->parts['{endLabel}'] = '';
            } elseif (!isset($this->parts['{beginLabel}'])) {
                $this->renderLabelParts();
            }
            if ($this->enableError === false) {
                $this->parts['{error}'] = '';
            }
            if ($this->inputTemplate) {
                $input = isset($this->parts['{input}']) ?
                    $this->parts['{input}'] : Html::activeTextInput($this->model, $this->attribute, $this->inputOptions);
                $this->parts['{input}'] = strtr($this->inputTemplate, ['{input}' => $input]);
            }
            if (!isset($this->parts['{input}'])) {
                $this->textInput();
            }
            if (!isset($this->parts['{label}'])) {
                $this->label();
            }
            if (!isset($this->parts['{error}'])) {
                $this->error();
            }
            if (!isset($this->parts['{hint}'])) {
                $this->hint(null);
            }
            $content = strtr($this->template, $this->parts);
        } elseif (!is_string($content)) {
            $content = call_user_func($content, $this);
        }

        return $this->begin() . "\n" . $content . "\n" . $this->end();
    }

    /**
     * 呈现字段容器的开始标记。
     * @return string
     */
    public function begin()
    {
        if ($this->form->enableClientScript) {
            $clientOptions = $this->getClientOptions();
            if (!empty($clientOptions)) {
                $this->form->attributes[] = $clientOptions;
            }
        }

        $inputID = $this->getInputId();
        $attribute = Html::getAttributeName($this->attribute);
        $options = $this->options;
        $class = isset($options['class']) ? (array)$options['class'] : [];
        $class[] = "field-$inputID";
        if ($this->model->isAttributeRequired($attribute)) {
            $class[] = $this->form->requiredCssClass;
        }
        $options['class'] = implode(' ', $class);
        if ($this->form->validationStateOn === Form::VALIDATION_STATE_ON_CONTAINER) {
            $this->addErrorClassIfNeeded($options);
        }
        $tag = ArrayHelper::remove($options, 'tag', 'div');

        return Html::beginTag($tag, $options);
    }

    /**
     * 呈现字段容器的结束标记。
     * @return string
     */
    public function end()
    {
        return Html::endTag(ArrayHelper::keyExists('tag', $this->options) ? $this->options['tag'] : 'div');
    }

    /**
     * 为[[attribute]]生成label标记
     * @param null|string|false $label 要使用的label
     * 如果为‘null’，则label通过[[Model::getAttributeLabel]]生成
     * 如果为‘false’，则生成的字段将不包含label部分
     * @param null|array $options 根据名称-值对的标记选项。
     * @return $this
     */
    public function label($label = null, $options = [])
    {
        if (is_bool($label)) {
            $this->enableLabel = $label;
            if ($label === false && $this->form->layout === 'horizontal') {
                Html::addCssClass($this->wrapperOptions, $this->horizontalCssClasses['offset']);
            }
            if ($label === false) {
                $this->parts['{label}'] = '';
            }
        } else {
            $this->enableLabel = true;
            $this->renderLabelParts($label, $options);
            $options = array_merge($this->labelOptions, $options);

            $options['label'] = $label ?? $this->model->getAttributeLabel($this->attribute);

            if ($this->_skipLabelFor) {
                $options['for'] = null;
            }

            $desc = ArrayHelper::remove($options, 'desc');
            if (boolval($desc)) {
                $spanOptions['title'] = $label;
                $spanOptions['data'] = [
                    'toggle' => 'popover',
                    'trigger' => 'hover',
                    'container' => 'body',
                    'content' => $desc
                ];

                $helpIcon = ' ' . Icon::i('fa-question-circle-o', ['class' => 'icon-sm text-muted']);

                $options['label'] = Html::tag('span', $options['label'] . $helpIcon, $spanOptions);

                $this->form->view->registerJs('$(\'[data-toggle="popover"]\').popover();');
            }

            $this->parts['{label}'] = Html::activeLabel($this->model, $this->attribute, $options);
        }

        return $this;
    }

    /**
     * 生成包含[[attribute]]的第一个验证错误的标记。<br>
     * 请注意，即使没有验证错误，此方法仍将返回空的错误标记。
     * @param array|false $options 根据名称-值对的标记选项。它将与[[errorOptions]]合并。
     * @return $this
     */
    public function error($options = [])
    {
        if ($options === false) {
            $this->parts['{error}'] = '';
            return $this;
        }
        $options = array_merge($this->errorOptions, $options);
        $this->parts['{error}'] = Html::error($this->model, $this->attribute, $options);

        return $this;
    }

    /**
     * 呈现提示标记。
     * @param string|bool $content 提示内容<br>
     * 如果为‘null’，则通过[[model::getAttributeHint()]]生成提示<br>
     * 如果为‘false’，则生成的字段将不包含提示部分<br>
     * 请注意这不是[[Html::encode()|encoded]]。
     * @param array $options 根据名称-值对的标记选项。这些将呈现为提示标记的属性。这些值将使用[[Html::encode()]]进行 HTML 编码。
     * @return $this
     */
    public function hint($content, $options = [])
    {
        if ($content === false) {
            $this->parts['{hint}'] = '';
            return $this;
        }

        $options = array_merge($this->hintOptions, $options);
        if ($content !== null) {
            $options['hint'] = $content;
        }
        $this->parts['{hint}'] = Html::activeHint($this->model, $this->attribute, $options);

        return $this;
    }

    /**
     * 呈现文本输入。
     * @param array $options
     * @return $this
     */
    public function textInput($options = [])
    {
        $options = array_merge($this->inputOptions, $options);

        if ($this->form->validationStateOn === Form::VALIDATION_STATE_ON_INPUT) {
            $this->addErrorClassIfNeeded($options);
        }

        $this->addAriaAttributes($options);
        $this->adjustLabelFor($options);
        $this->parts['{input}'] = Html::activeTextInput($this->model, $this->attribute, $options);

        return $this;
    }

    /**
     * 将小部件呈现为字段的输入。
     * @param string $class 小部件类名
     * @param array $config 将用于初始化小部件的名称-值对。
     * @return $this
     * @throws \Exception
     */
    public function widget($class, $config = [])
    {
        /* @var $class \yii\base\Widget */
        $config['model'] = $this->model;
        $config['attribute'] = $this->attribute;
        $config['view'] = $this->form->getView();
        if (is_subclass_of($class, 'yii\widgets\InputWidget')) {
            foreach ($this->inputOptions as $key => $value) {
                if (!isset($config['options'][$key])) {
                    $config['options'][$key] = $value;
                }
            }
            $config['field'] = $this;
            if (!isset($config['options'])) {
                $config['options'] = [];
            }
            if ($this->form->validationStateOn === Form::VALIDATION_STATE_ON_INPUT) {
                $this->addErrorClassIfNeeded($config['options']);
            }

            $this->addAriaAttributes($config['options']);
            $this->adjustLabelFor($config['options']);
        }

        $this->parts['{input}'] = $class::widget($config);

        return $this;
    }

    /**
     * 根据输入选项调整标签的“for”属性。
     * @param array $options
     */
    protected function adjustLabelFor($options)
    {
        if (!isset($options['id'])) {
            return;
        }
        $this->_inputId = $options['id'];
        if (!isset($this->labelOptions['for'])) {
            $this->labelOptions['for'] = $options['id'];
        }
    }

    /**
     * 返回字段的js选项。
     * @param null $validator
     * @param null $label
     * @return array
     */
    protected function getClientOptions($validator = null, $label = null)
    {
        $attribute = Html::getAttributeName($this->attribute);
        if (!in_array($attribute, $this->model->activeAttributes(), true)) {
            return [];
        }

        $clientValidation = $this->isClientValidationEnabled();
        $ajaxValidation = $this->isAjaxValidationEnabled();

        if ($clientValidation) {
            $validators = [];
            if (boolval($validator) && property_exists($this->model, 'attrLabel')) {
                $this->model->attrLabel = [$this->attribute => $label];
                foreach ($validator as $child) {
                    $type = (string)$child->attributes()->type;
                    $params = [];
                    foreach ($child->attributes() as $key => $value) {
                        if ($key !== 'type') {
                            switch ((string)$value) {
                                case 'true':
                                    $val = true;
                                    break;
                                case 'false':
                                    $val = false;
                                    break;
                                default:
                                    $val = (string)$value;
                            }

                            $params[$key] = $val;
                        }
                    }
                    $validator = Validator::createValidator($type, $this->model, $this->attribute, $params);
                    $js = $validator->clientValidateAttribute($this->model, $this->attribute, $this->form->view);
                    $validators[] = $js;
                }
            } else {
                foreach ($this->model->getActiveValidators($attribute) as $validator) {
                    /* @var $validator Validator */
                    $js = $validator->clientValidateAttribute($this->model, $attribute, $this->form->getView());
                    if ($validator->enableClientValidation && $js != '') {
                        if ($validator->whenClient !== null) {
                            $js = "if (({$validator->whenClient})(attribute, value)) { $js }";
                        }
                        $validators[] = $js;
                    }
                }
            }

        }

        if (!$ajaxValidation && (!$clientValidation || empty($validators))) {
            return [];
        }

        $options = [];

        $inputID = $this->getInputId();
        $options['id'] = Html::getInputId($this->model, $this->attribute);
        $options['name'] = $this->attribute;

        $options['container'] = isset($this->selectors['container']) ? $this->selectors['container'] : ".field-$inputID";
        $options['input'] = isset($this->selectors['input']) ? $this->selectors['input'] : "#$inputID";
        if (isset($this->selectors['error'])) {
            $options['error'] = $this->selectors['error'];
        } elseif (isset($this->errorOptions['class'])) {
            $options['error'] = '.' . implode('.', preg_split('/\s+/', $this->errorOptions['class'], -1, PREG_SPLIT_NO_EMPTY));
        } else {
            $options['error'] = isset($this->errorOptions['tag']) ? $this->errorOptions['tag'] : 'span';
        }

        $options['encodeError'] = !isset($this->errorOptions['encode']) || $this->errorOptions['encode'];
        if ($ajaxValidation) {
            $options['enableAjaxValidation'] = true;
        }
        foreach (['validateOnChange', 'validateOnBlur', 'validateOnType', 'validationDelay'] as $name) {
            $options[$name] = $this->$name === null ? $this->form->$name : $this->$name;
        }

        if (!empty($validators)) {
            $options['validate'] = new JsExpression('function (attribute, value, messages, deferred, $form) {' . implode('', $validators) . '}');
        }

        if ($this->addAriaAttributes === false) {
            $options['updateAriaInvalid'] = false;
        }

        return array_diff_assoc($options, [
            'validateOnChange' => true,
            'validateOnBlur' => true,
            'validateOnType' => false,
            'validationDelay' => 500,
            'encodeError' => true,
            'error' => '.help-block',
            'updateAriaInvalid' => true,
        ]);
    }

    /**
     * 检查是否为字段启用了客户端验证。
     * @return bool
     */
    protected function isClientValidationEnabled()
    {
        return $this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation;
    }

    /**
     * 检查是否为字段启用了ajax验证。
     * @return bool
     */
    protected function isAjaxValidationEnabled()
    {
        return $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation;
    }

    /**
     * 返回此表单域的 input 元素的‘id’属性。
     * @return string
     */
    protected function getInputId()
    {
        return $this->_inputId ?: Html::getInputId($this->model, $this->attribute);
    }

    /**
     * 向输入选项添加aria属性。
     * @param array $options 输入选项
     */
    protected function addAriaAttributes(&$options)
    {
        if ($this->addAriaAttributes) {
            if (!isset($options['aria-required']) && $this->model->isAttributeRequired($this->attribute)) {
                $options['aria-required'] = 'true';
            }
            if (!isset($options['aria-invalid']) && $this->model->hasErrors($this->attribute)) {
                $options['aria-invalid'] = 'true';
            }
        }
    }

    /**
     * 如果需要，将验证类添加到输入选项。
     * @param array $options 输入选项
     */
    protected function addErrorClassIfNeeded(&$options)
    {
        $attributeName = Html::getAttributeName($this->attribute);

        if ($this->model->hasErrors($attributeName)) {
            Html::addCssClass($options, $this->form->errorCssClass);
        }
    }

    /**
     * @param array $instanceConfig 传递给此实例的构造函数的配置
     * @return array 此实例的特定于布局的默认配置
     */
    protected function createLayoutConfig($instanceConfig)
    {
        $config = [
            'hintOptions' => [
                'tag' => 'p',
                'class' => 'help-block',
            ],
            'errorOptions' => [
                'tag' => 'p',
                'class' => 'help-block help-block-error',
            ],
            'inputOptions' => [
                'class' => 'form-control',
            ],
        ];

        $layout = $instanceConfig['form']->layout;

        if ($layout === 'horizontal') {
            $config['template'] = "{label}\n{beginWrapper}\n{input}\n{error}\n{endWrapper}\n{hint}";
            $cssClasses = array_merge([
                'offset' => 'col-sm-offset-3',
                'label' => 'col-sm-3',
                'wrapper' => 'col-sm-6',
                'error' => '',
                'hint' => 'col-sm-3',
            ], $this->horizontalCssClasses);
            if (isset($instanceConfig['horizontalCssClasses'])) {
                $cssClasses = ArrayHelper::merge($cssClasses, $instanceConfig['horizontalCssClasses']);
            }
            $config['horizontalCssClasses'] = $cssClasses;
            $config['wrapperOptions'] = ['class' => $cssClasses['wrapper']];
            $config['labelOptions'] = ['class' => 'control-label ' . $cssClasses['label']];
            $config['errorOptions']['class'] = trim('help-block help-block-error ' . $cssClasses['error']);
            $config['hintOptions']['class'] = 'help-block ' . $cssClasses['hint'];
        } elseif ($layout === 'inline') {
            $config['labelOptions'] = ['class' => 'sr-only'];
            $config['enableError'] = false;
        }

        return $config;
    }

    /**
     * @param string|null $label 用于模型label 的 label 或 null
     * @param array $options 标签选项
     */
    protected function renderLabelParts($label = null, $options = [])
    {
        $options = array_merge($this->labelOptions, $options);
        if ($label === null) {
            if (isset($options['label'])) {
                $label = $options['label'];
                unset($options['label']);
            } else {
                $attribute = Html::getAttributeName($this->attribute);
                $label = Html::encode($this->model->getAttributeLabel($attribute));
            }
        }
        if (!isset($options['for'])) {
            $options['for'] = Html::getInputId($this->model, $this->attribute);
        }
        $this->parts['{beginLabel}'] = Html::beginTag('label', $options);
        $this->parts['{endLabel}'] = Html::endTag('label');
        if (!isset($this->parts['{labelTitle}'])) {
            $this->parts['{labelTitle}'] = $label;
        }
    }
}
